package geographer;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.io.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

/**
 * Project #3
 * CS 2334, 010
 * 03/26/2014
 * <P>
 * This class abstracts the database away. It contains methods to retrieve and print 
 * objects from the database.
 * </P>
 * @version 3.0
 */
public class Database {
	
	/*Map that contains the keys for the regions*/
	private static Map<Region, List<String>> keyMap;
	
	/*The Map that contains the various regions*/
	public static Map<String, GeographicRegion> regions;
	
	//Database is a static class
	public Database(){
		
		//Initialize the Maps
		keyMap = new HashMap<Region, List<String>>();
		regions = new LinkedHashMap<String, GeographicRegion>();
		
		//Attatch the two maps via the keys
		for(Region r : Region.values())
			keyMap.put(r, new LinkedList<String>());
	}

	/**
	 * Initialize the database
	 * <P>
	 * Algorithm:<br>
	 * 1. Take in user input <br>
	 * 2. Process data locations <br>
	 * 3. Sends each file location to scan(file).<br>
	 * </P>
	 * @throws	IOException     if the file is not found
	 * @throws ClassNotFoundException 
	 */
	static void initialize() throws IOException, ClassNotFoundException {
		//new Database();
		
		//BufferedReader that will read the console
		BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		
		//String array that holds the data file locations
		String[] files = new String[4];
		
		//Takes the user input for the type of file
		int fileInput;
		
		//Get the type of file to be input.
		System.out.println("What would you like to load?\n"
				+ "\t(1) Text Files\n"
				+ "\t(2) Binary File\n");
		do{
			fileInput = Integer.parseInt(in.readLine().trim());
			if(fileInput<1||fileInput>2)
			{
				System.out.println("Your response was invalide. Try again.");
			}
		}while(fileInput<1||fileInput>2);
		
		//Perform approriate read actions depending on the type of file
		if(fileInput == 1)
		{
		/*
		//Take in user input and process it
			files[0] = in.readLine().trim();
		System.out.println("Enter the country file directory:");
			files[1] = in.readLine().trim();
		System.out.println("Enter the city file directory:");
			files[2] = in.readLine().trim();
		System.out.println("Enter the place file directory:");
			files[3] = in.readLine().trim();
		*/
		files[0] = "C:\\Users\\Troy\\Downloads\\continents.csv";
		files[1] = "C:\\Users\\Troy\\Downloads\\countries.csv";
		files[2] = "C:\\Users\\Troy\\Downloads\\cities.csv";
		files[3] = "C:\\Users\\Troy\\Downloads\\places.csv";
		/*
		files[0] = "C:\\Users\\Austin\\Downloads\\continents.csv";
		files[1] = "C:\\Users\\Austin\\Downloads\\countries.csv";
		files[2] = "C:\\Users\\Austin\\Downloads\\cities.csv";
		files[3] = "C:\\Users\\Austin\\Downloads\\places.csv";
			*/
		//Scan files
		for(int i=0; i<files.length; ++i)
			scan(files[i], Region.values()[i]);
		}
		else 
		{
			//Perform the binary scan
			binaryScan();
		}
		
	}//end initialize
	
	/**
	 * Interactions with the user
	 * <P>
	 * Algorithm:<br>
	 * 1. Handles User Input
	 * 2. Determines which data set to analyze
	 * 3. Calls appropriate sort function
	 * 4. Calls print(List, bool).
	 * -OR-
	 * 4. Searches, then calls region.toString().
	 * 5. Loop until quit
	 * </P>
	 * @throws	IOException   if the file is not found
	 * <dt><b>Conditions:</b> 
	 * <dd>POST - Program loops until the user quits
	 */
	@SuppressWarnings({"unchecked", "rawtypes"})
	static void interact() throws IOException {
		//Buffered Reader to read the console
		BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		
		//The list that will hold the list of indicated regions to perform action on
		List indicatedRegion = new ArrayList();
		
		//Holds the input of the user
		String[] input = new String[4];
		String gdLabel = null;
		
		//Loop that will repeat until user quits
		do{
			//Show options to the user
			System.out.println("What type of geographic data are you interested in?\n" 
					+"\t(1) All continents.\n"
					+"\t(2) All countries.\n"
					+"\t(3) All cities.\n"
					+"\t(4) All places of interest.\n"
					+"\t(5) All countries within a specified continent.\n"
					+"\t(6) All cities wihtin a specified country.\n");
			
			//Loop if the user does not enter a valid input
			do{
				input[0] = in.readLine().trim().toUpperCase(); //process input
				if(!input[0].matches("[1-6]"))
					System.out.println("That is incorrect input.");//inform user of mistake
				else break;//if correct, quit loop
			}
			while(true);
			
			//Process the input and fill the indicatedRegion ArrayList with correct information
			switch(input[0]){
				case "1": //If the user chooses the continents option
					indicatedRegion = keyMap.get(Region.CONTINENT);
					gdLabel = "All Continents";
				break;
				case "2"://if the user chooses the countries option
					indicatedRegion = keyMap.get(Region.COUNTRY);
					gdLabel = "All Countries";
				break;
				case "3"://If the user chooses the cities option
					indicatedRegion = keyMap.get(Region.CITY);
					gdLabel = "All Cities";
				break;
				case "4": //If the user chooses the places option
					indicatedRegion = keyMap.get(Region.PLACE);
					gdLabel = "All Places";
				break;
				case "5"://If the user chooses all countries option
					System.out.println("All countries from which continent?");
					String indicatedContinent = in.readLine().trim().toUpperCase();
					gdLabel = "All Coutries in "+indicatedContinent;
					
					//Fill indicatedRegion with Countries by the specified continent
					indicatedRegion = ((Continent) regions.get(indicatedContinent)).getSubregions();
				break;
				case "6"://If the user chooses the all cities option
					System.out.println("All cities from which country?");
					String indicatedCountry = in.readLine().trim().toUpperCase();
					gdLabel = "All Cities in "+indicatedCountry;
					
					//Fill the indicaatedRegion ArrayList based on the indicated country
					indicatedRegion = ((Country) regions.get(indicatedCountry)).getSubregions();
				break;
			}
			
			//Show user data sorting options
			System.out.println("How should the data be sorted?\n"
					+ "\t(AR) Area\n" + (!input[0].matches("4") ?
					  "\t(PO) Population\n" :"")
					+ "\t(LE) Lexicographically\n"
					+ "\t(RA) Random\n" + (input[0].matches("(?:3|6)") ? 
					  "\t(EL) Elevation\n"
					+ "\t(LA) Lattitude\n"
					+ "\t(LO) Longitude" :""));
			//process the validity of the user input by regular expression
			do{
				input[1] = in.readLine().trim().toUpperCase();
				if(!(input[0]+input[1]).matches(".(?:AR|LE|RA)|(1|2|3|5|6)(?:PO)|(3|6)(?:EL|LA|LO)"))
					System.out.println("The input was incorrect.");//Show user the error
				else break;
			}
			while(true);
			
			//Perform a sort based on the user indicated sort method
			switch(input[1]){
				case "PO": Collections.sort(indicatedRegion, GeographicRegion.PO); break;
				case "AR": Collections.sort(indicatedRegion, GeographicRegion.AR);  break; 
				case "EL": Collections.sort(indicatedRegion, City.EL); 			    break;
				case "LA": Collections.sort(indicatedRegion, City.LA); 			    break;
				case "LO": Collections.sort(indicatedRegion, City.LO); 			    break;
				case "RA": Collections.shuffle(indicatedRegion); 				    break;
				case "LE": Collections.sort(indicatedRegion); 					    break;
			}
			
			//Show user output options
			System.out.println("How should the data be output?\n"
					+"\t(PF) Print to a file.\n"
					+"\t(PS) Print to the console.\n"
					+"\t(SP) Search for a particular region.\n"
					+"\t(OO) Object Output.\n"
					+ ((input[0]+input[1]).matches("(?:1|2|3|4|5|6)(AR)|(?:1|2|3)(PO)|(?:3|6)(LA|LO)")?"\t(GD) Graphical Display.":""));
			
			//Process the validity of the user input by regular expression
			do{
				input[2] = in.readLine().trim().toUpperCase();
				if(!input[2].matches("(?:PF|PS|SP|OO|GD)"))
					System.out.println("The input was incorrect");//Show user the error
				else break;
			}
			while(true);
			
			//Process the output based on user input
			switch(input[2]){
				case "PS": print(indicatedRegion); break; //Print to the screen
				case "PF": //Print to a file with user input name
					System.out.println("What should the file name be?");
					String fileName = in.readLine().trim().toUpperCase();
					print(indicatedRegion, fileName); 
				break;
				case "SP": //Search further into the database
					System.out.println("For which region are you searching?");
					String searchKey = in.readLine().trim().toUpperCase();
					
					//Search the indecatedRegion for the key; display if it is found
					GeographicRegion region = regions.get(searchKey);
					if(region != null)
						System.out.println("\n"+ region +"\n");
					else
						System.out.println("key not found");//Show error if needed
				break;
				case "OO":
					//Object Output
					outputBinary();
				break;
				case "GD":
					//Perform Setup for the appropriate graphical display
					switch(input[1]){
						case "PO": 
							if(input[0].matches("(?:1|2|3)"))
							{
								//Instantiate the frame and panel
								JFrame barFrame = new JFrame("GeoGrapher :: Bar");
								JPanel p = new JPanel();
								JPanel graph = new JPanel(new BorderLayout());
								p.setPreferredSize(new java.awt.Dimension(indicatedRegion.size()*125, 300));
								
								//Configure the layout and add the panel elements
								p.setLayout(new GridLayout(1,0));
								Bar.setHeightReference(regions.get(indicatedRegion.get(0)).getPopulation());
								for(String r: (String[]) indicatedRegion.toArray(new String[indicatedRegion.size()]))
									p.add(new Bar(regions.get(r).getName(), regions.get(r).getPopulation()));
									
								//Add elements to the frame and show the frame
								graph.add(new JScrollPane(p), BorderLayout.CENTER);
								graph.add(new JLabel(gdLabel+" by Population", null, JLabel.CENTER), BorderLayout.NORTH);
								barFrame.add(graph);
								barFrame.setSize(800, 550);
								barFrame.setLocationRelativeTo(null);
								barFrame.setVisible(true);
							}
							else
								//Inform the user of an error
								System.out.println("This path does not have a graphical display.");
						break;
						case "AR": 
							if(input[0].matches("(?:1|2|3|4)"))
							{
								//Instantiate the panel and the frame
								JFrame barFrame = new JFrame("GeoGrapher :: Bar");
								JPanel p = new JPanel();
								JPanel graph = new JPanel(new BorderLayout());
								p.setPreferredSize(new java.awt.Dimension(indicatedRegion.size()*125, 300));
							
								//configure layout and add elements to the panel
								p.setLayout(new GridLayout(1,0));
								Bar.setHeightReference(regions.get(indicatedRegion.get(0)).getArea());
								for(String r: (String[]) indicatedRegion.toArray(new String[indicatedRegion.size()]))
									p.add(new Bar(regions.get(r).getName(), regions.get(r).getArea()));
								
								//Add elements to the frame and show the frame
								graph.add(new JScrollPane(p), BorderLayout.CENTER);
								graph.add(new JLabel(gdLabel+" by Area", null, JLabel.CENTER), BorderLayout.NORTH);
								barFrame.add(graph);
								barFrame.setSize(800, 550);
								barFrame.setLocationRelativeTo(null);
								barFrame.setVisible(true);
							}
							else
							{
								//Instantiate the frame
								JFrame frame = new JFrame("GeoGrapher :: Partition");
								frame.setSize(800, 700);
								
								//Calculate the values to be used with the graph
								//The width of the graph
								int width = frame.getWidth()/2;
								
								//The top right corner of the graph
								int posX = frame.getWidth()/4;
								
								//The overall height of the graph
								int height = frame.getHeight()-20;
								
								//Ready the list to hold the data
								List dataList = new ArrayList();
								
								//Fill the data list
								for(String r: (String[]) indicatedRegion.toArray(new String[indicatedRegion.size()]))
								{
									dataList.add(regions.get(r));
								}
								
								//Instantiate, add, and show the frame with the graph
								PartitionBar bar = new PartitionBar(width,height,posX,dataList,"Population");
								JPanel graph = new JPanel(new BorderLayout());
								graph.add(new JLabel(gdLabel+" by Area", null, JLabel.CENTER), BorderLayout.NORTH);
								graph.add(bar, BorderLayout.CENTER);
								frame.add(graph);
								frame.setLocationRelativeTo(null);
								frame.setVisible(true);
							}
						break;
						case "LA":  
						case "LO":  
							if(input[0].matches("(?:3|6)"))
							{
								//Instantiate the frame
								JFrame worldFrame = new JFrame("Geographer :: World Map");
								
								//The world map to be displayed
								WorldMap map =  new WorldMap();
								
								//Loop through the hash map to get the relevante values
								for(String r: (String[]) indicatedRegion.toArray(new String[indicatedRegion.size()]))
								{
									//Collect then City value
									City city = (City)regions.get(r);
									
									//Add the points to the map
									try
									{
										map.addPoint(city.getName(),city.getLat(),city.getLon());
									}catch(NullPointerException e)
									{
										//Inform the user of an error
										System.out.println(city.getName()+" does not have an associated latitude or longitude.");
									}
								}
								
								//Show the map
								JPanel graph = new JPanel(new BorderLayout());
								graph.add(map, BorderLayout.CENTER);
								graph.add(new JLabel(gdLabel+" on the World Map", null, JLabel.CENTER), BorderLayout.NORTH);
								worldFrame.add(map);
								worldFrame.setSize(1300, 685);
								worldFrame.setLocationRelativeTo(null);
								worldFrame.setVisible(true);
							}
							else
								//Inform the user of an error
								System.out.println("This path does not have a graphical display.");
						break;
						default:  //Do nothing
						break;
					}
				break;
			}
			
			//Show user continue options
			System.out.println("Would you like to continue?\n"
					+"\t(C) Continue\n"
					+ "\t(Q) Quit");
			
			//Process user input validity by regular expression
			do{
				input[3] = in.readLine().trim().toUpperCase();
				if(!input[3].matches("(?:C|Q)"))
					System.out.println("The input was incorrect.");//Show user error
				else break;
			}
			while(true);
		}
		while(!input[3].equals("Q"));
		
		//Close the buffered reader
		in.close();
	}//end interact
	
	/*
	 * Output an IO file
	 * <P>
	 * Algorithm:<br>
	 * 1. Create a OOS and wrtie the object to the file
	 * </P>
	 * @param	indicatedRegion	The region we want to write to the file
	 */
	
	public static void outputBinary() throws IOException {
		//Reader to take the user's input
		//BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		
		//Collect user input
		//System.out.println("What would you like to name the file?");
		//String fileName = in.readLine().trim();
		
		//Write the object to the specified file
		try {
			ObjectOutputStream myFOS = new ObjectOutputStream(new FileOutputStream("geographer"));
			myFOS.writeObject(regions);
			myFOS.writeObject(keyMap);
			myFOS.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/*Keep track of the current region being processed*/
   public static enum Region {
		CONTINENT, COUNTRY, CITY, PLACE, POINT
	}//end enum Region
	
	/**
	 * Scan the file
	 * <P>
	 * Algorithm:<br>
	 * 1. Send the data to <code>inject</code> to be put into the database
	 * </P>
	 * @param   file    A String that holds the file name of the data
	 * @param   region   The region that will be filled with the data
	 * @throws	IOException     if the file is not found
	 */
	public static void scan(String file, Region region) throws IOException {
		
		//Create the buffered reader to scan the file
		BufferedReader br = new BufferedReader(new FileReader(file));
		
		//Inject the contents of the file
		while(inject(br.readLine(), region));
		
		//Close the buffered reader
		br.close();
	}//end scan

    public Map<String, GeographicRegion> getRegions(){
		return regions;
	}
    public static List<String> getKeys(Region r){
		return keyMap.get(r);
	}
	
	/**
	 * Scans a binary data for information to put into the database.
	 * <P>
	 * Algorithm:<br>
	 * 1.Read a line of the file.<br>
	 * 2.Parse the line to be usable by the database.<br>
	 * 3.Send the information to be put into the database.<br>
	 * </P>
	 * @param file    The location of the file to be read
	 * @param region  The region that the data belongs to
	 * @throws IOException     If the file cannot be found
	 * @throws ClassNotFoundException 
	 */
	@SuppressWarnings("unchecked")
	public static void binaryScan() throws IOException, ClassNotFoundException
	{
		try{
		ObjectInputStream oIS = new ObjectInputStream(new FileInputStream("geographer"));
		regions = (Map<String, GeographicRegion>) oIS.readObject();
		keyMap = (Map<Region, List<String>>) oIS.readObject();
		oIS.close();
		}catch (Exception e){
			//System.out.println("The file was not read correctly.");
			e.printStackTrace();
		}
		
	}//end binaryScan
	
	/**
	 * Inject the contents of a file into the database
	 * <P>
	 * Algorithm:<br>
	 * 1. Determine if the String input is valid
	 * 2. Split the data of from the file by a separator<br>
	 * 3. Put the data into the database based on the indicated region
	 * </P>
	 * @param   line     The String containing the data to be put into the database
	 * @param   region   The region that will be taking the data
	 * @return	boolean	Success of operation
	 */
	public static boolean inject(String line, Region region) {
		
		//Return as unsuccessful if the line is null
		if(line == null)
			return false;
		
		//Split the line by the separator
		String[] data = line.split(", ");
		String name = data[0].toUpperCase();
		
		//Put the contents of the line into the database based on the enumerated region
		switch(region){
			case CONTINENT:
				keyMap.get(Region.CONTINENT).add(name);
				regions.put(name, new Continent(data));
			break;
			case COUNTRY:
				Continent cn = (Continent) regions.get(data[3].toUpperCase());
				if(cn == null)
					break;
				
				cn.add(name);
				keyMap.get(Region.COUNTRY).add(name);
				regions.put(name, new Country(data));
			break;
			case CITY:
				Country co = (Country) regions.get(data[3].toUpperCase());
				if(co == null)
					break;
				
				co.add(name);
				keyMap.get(Region.CITY).add(name);
				regions.put(name, new City(data));
			break;
			case PLACE:
				keyMap.get(Region.PLACE).add(name);
				regions.put(name, new Place(data));
			break;
			case POINT:
				keyMap.get(Region.POINT).add(name);
				regions.put(name, new Point(data));
			break;
		}
		
		return true; //Return as successful
	}//end inject
	
	/**
	 * This is an overloaded method to determine if the user wants the information printed
	 * to the screen. If so, this method calls its overloaded companion with a null file name
	 * to tell it to put it to the screen.
	 * @param regions  The list of regions to be printed
	 * @throws IOException  if the file is not found
	 */
	static void print(List<?> regions) throws IOException {
		print(regions, null);
	}//end print
	
	

	
	/**
	 * Print the results to the user chosen medium
	 * <P>
	 * Algorithm:<br>
	 * 1. If the <code>fileName</code> is null, print the results to the screen
	 * 2. Otherwise write the results to a file.
	 * </P>
	 * @param	regions   The List containing the contents of the results
	 * @param  fileName    A String that holds the name of the file the user wants to output to
	 * @throws IOException    if the file is not found.
	 */
	public static void print(List<?> regions, String fileName) throws IOException {
		
		//If the fileName is null, print the contents to the screen
		if(fileName == null){
			System.out.println();
			for(String r: regions.toArray(new String[regions.size()]))
				System.out.println(Database.regions.get(r));
			System.out.println();
			return;
		}
		//BufferedWriter that will open the file
		BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
		
		//Put the contents into the file
		for(Object region: regions){
			writer.write(region.toString());
			writer.newLine();
		}
		
		//close the writer
		writer.close();
	}//end print
	
	/**
	 * Add the specified region to the database 
	 * <P>
	 * Algorithm:<br>
	 * 1. Get the name from the data
	 * 2. Depending on the type of the data, add the data in different ways
	 * </P>
	 * @param	r	The region we want to add
	 * @param	data	Some parameters for the region
	 */
	public void add(Region r, String[] data){
		
		//Get the name of the region
		String name = data[0].toUpperCase();
		
		//Put the contents of the line into the database based on the enumerated region
		switch(r){
			case CONTINENT:
				keyMap.get(Region.CONTINENT).add(name);
				regions.put(name, new Continent(data));
			break;
			case COUNTRY:
				Continent cn = (Continent) regions.get(data[3].toUpperCase());
				if(cn == null)
					break;
				cn.add(name);
				keyMap.get(Region.COUNTRY).add(name);
				regions.put(name, new Country(data));
			break;
			case CITY:
				Country co = (Country) regions.get(data[6].toUpperCase());
				if(co == null)
					break;
				
				co.add(name);
				keyMap.get(Region.CITY).add(name);
				regions.put(name, new City(data));
			break;
			case PLACE:
				keyMap.get(Region.PLACE).add(name);
				regions.put(name, new Place(data));
			break;
			case POINT:
				keyMap.get(Region.POINT).add(name);
				regions.put(name, new Point(data));
			break;
		}
	}
	
	/**
	 * Delete the specified region from the database
	 * <P>
	 * Algorithm:<br>
	 * 1. Find the region
	 * 2. Delete any subregions if it has any
	 * </P>
	 * @param reg	The region we want to delete
	 * @param key
	 */
	public void delete(Region reg, String key)
	{
		switch(reg){
		case CONTINENT:
			for(String country:((Continent)regions.get(key)).getSubregions()){

					for(String city:((Country)regions.get(country)).getSubregions()){
						regions.remove(city);
						keyMap.get(Region.CITY).remove(city);
					}
				
				regions.remove(country);
				keyMap.get(Region.COUNTRY).remove(country);
				
			}
		break;	
		case COUNTRY:
			for(String city:((Country)regions.get(key)).getSubregions()){
				regions.remove(city);
				keyMap.get(Region.CITY).remove(city);
			}
		case CITY:
		case PLACE:
		case POINT:
		break;
		}
		
		for(String r : keyMap.get(Region.POINT))
			
			if(((Point)regions.get(r)).getBelonging().remove(key))
				if(((Point)regions.get(r)).getBelonging().isEmpty()){
					regions.remove(r);
					keyMap.get(Region.POINT).remove(r);
				}
			
		
		for(String r : keyMap.get(Region.PLACE))
			if(((Place)regions.get(r)).getOwners().remove(key))
				if(((Place)regions.get(r)).getOwners().isEmpty()){
					regions.remove(r);
					keyMap.get(Region.PLACE).remove(r);
				}
		
		regions.remove(key);
		keyMap.get(reg).remove(key);
				
			
	}//end delete
	
	/**
	 * Edit the entry
	 * <P>
	 * Algorithm<br>
	 * 1. Switch the region and call a different edit depending on the case.
	 * </P>
	 * @param reg
	 * @param data
	 * @param key
	 */
	
	public void edit(Region reg, String[] data, String key)
	{
		switch(reg){
			case CONTINENT:
				((Continent)regions.get(key)).edit(data);
			break;
			case COUNTRY:
				((Country)regions.get(key)).edit(data);
			break;
			case CITY:
				((City)regions.get(key)).edit(data);
			break;
			case PLACE:
				((Place)regions.get(key)).edit(data);
			break;
			case POINT:
				((Point)regions.get(key)).edit(data);
			break;
		}
	}
	
}//end Database
